/*
Continuous Testing Toolkit
Copyright (C) 2009 Paul R. Holser, Jr.

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/

package org.infinitest.toolkit;

import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.util.Arrays;
import static java.io.File.*;

import org.apache.commons.lang.ObjectUtils;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import static org.apache.commons.io.FileUtils.*;
import static org.apache.commons.lang.SerializationUtils.*;
import static org.infinitest.toolkit.ArrayUtils.*;

/**
 * "Golden master" testing is useful for recording the results of methods that are difficult to test, manually
 * verifying that the result meets expectations, and thereafter automatically verifying that the result continues
 * to meet those expectations.
 *
 * Here's how to use this class:
 *
 * Instantiate a "golden master" repository using {@link #at(File)}.
 *
 * Use the following to record a complex value:
 *     <code>assertThat(complexValue, <strong>master.approving()</strong>);</code>
 *
 * The test will fail, and will indicate what value was recorded.  Manually verify this value.
 *
 * When you are satisfied that the value is acceptable, call:
 *     <code>assertThat(complexValue, <strong>master.approved()</strong>);</code>
 *
 * This will assert that the value matches the value recorded during the "approving" phase.
 *
 * Any {@link Serializable} object can be recorded.  "Golden masters" are recorded as files in directory
 * structures corresponding to the class/method in which they are used, beneath the directory given to the
 * "golden master" repository.  You can use only one "golden master" in any given test method.
 *
 * @author <a href="http://www.pholser.com">Paul Holser</a>
 */
public class GoldenMaster {
    private final File directory;

    private GoldenMaster(File aDirectory) {
        if (!aDirectory.exists())
            throw new IllegalArgumentException(aDirectory + " does not exist");

        if (!aDirectory.isDirectory())
            throw new IllegalArgumentException(aDirectory + " is not a directory");

        if (!aDirectory.canRead())
            throw new IllegalArgumentException(aDirectory + " cannot be read");

        if (!aDirectory.canWrite())
            throw new IllegalArgumentException(aDirectory + " cannot be written to");

        directory = aDirectory;
    }

    /**
     * Creates and returns a new "golden master" repository on the file system rooted at the directory with
     * the given path.
     *
     * @param directoryPath path to a directory to hold golden masters
     * @see GoldenMaster#at(File)
     */
    public static GoldenMaster at(String directoryPath) {
        return at(new File(directoryPath));
    }

    /**
     * Creates and returns a new "golden master" repository on the file system rooted at the given directory.
     *
     * @param aDirectory the directory in which to deposit and retrieve "golden masters"
     * @return a golden master repository
     * @throws IllegalArgumentException if the given directory does not exist, is not a directory, or cannot
     * be read from or written to
     */
    public static GoldenMaster at(File aDirectory) {
        return new GoldenMaster(aDirectory);
    }

    /**
     * @return a matcher which, when triggered, records the value it is asked to match, and fails the calling
     * test with a message that indicates that the value should be manually verified.
     * @throws AssertionFailedError always
     */
    public Matcher<Serializable> approving() {
        final Key key = makeKey();

        return new BaseMatcher<Serializable>() {
            public boolean matches(Object target) {
                try {
                    add(key, (Serializable) target);
                    return false;
                }
                catch (IOException ex) {
                    throw new RuntimeException(ex);
                }
            }

            public void describeTo(Description description) {
                description.appendText("Manually verify that the recorded golden master for key [")
                    .appendValue(key)
                    .appendText("] meets your expectations.")
                    .appendText(" Change the approving() call to approved() when you are satisfied.");
            }
        };
    }

    /**
     * @return a matcher which, when triggered, compares the value it is asked to match against a previously
     * recorded value, failing the calling test if the values are not {@linkplain Object#equals(Object) equal}.
     * Arrays are compared via {@link Arrays#equals(Object[], Object[])}.
     * @throws AssertionFailedError if recorded value and new value do not match
     */
    public Matcher<Serializable> approved() throws IOException {
        final Key key = makeKey();
        final Object master = get(key);

        return new BaseMatcher<Serializable>() {
            public boolean matches(Object target) {
                if (master == null)
                    return false;
                return master.getClass().isArray()
                    ? arraysAreEqual(target, master)
                    : ObjectUtils.equals(target, master);
            }

            public void describeTo(Description description) {
                if (master == null)
                    description.appendText("that there be a golden master for key [").appendValue(key).appendText("]");
                else {
                    description.appendText("a string that matches the golden master for key [").appendValue(key)
                        .appendText("], namely: [").appendValue(master).appendText("]");
                }
            }
        };
    }

    void reset() throws IOException {
        cleanDirectory(directory);
    }

    Key key() {
        return new Key(directory, new Throwable().getStackTrace()[1]);
    }

    private Key makeKey() {
        return new Key(directory, new Throwable().getStackTrace()[2]);
    }

    private Object get(Key key) throws IOException {
        File file = key.toFile();
        if (!file.exists())
            return null;

        byte[] pickled = readFileToByteArray(file);
        return deserialize(pickled);
    }

    private void add(Key key, Serializable master) throws IOException {
        byte[] pickled = serialize(master);
        writeByteArrayToFile(key.toFile(), pickled);
    }

    public static class Key {
        private final File root;
        private final String className;
        private final String methodName;

        Key(File root, StackTraceElement caller) {
            this.root = root;
            this.className = caller.getClassName().replace('.', separatorChar);
            this.methodName = caller.getMethodName();
        }

        @Override
        public boolean equals(Object that) {
            if (this == that)
                return true;
            if (that == null || !getClass().equals(that.getClass()))
                return false;

            Key other = (Key) that;
            return root.equals(other.root) && className.equals(other.className) && methodName.equals(other.methodName);
        }

        @Override
        public int hashCode() {
            return root.hashCode() ^ className.hashCode() ^ methodName.hashCode();
        }

        @Override
        public String toString() {
            return root.getAbsolutePath() + separatorChar + className + separatorChar + methodName;
        }

        File toFile() {
            File dir = new File(root, className);
            dir.mkdirs();
            return new File(dir, methodName);
        }
    }
}
